﻿/// Copyright (c) Microsoft Corporation.  All rights reserved.
using System;
using System.Collections;
using System.ComponentModel;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Configuration;
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using System.Reflection;
using System.Text.RegularExpressions;
using System.IO;
using System.Text;

namespace BuddyBuildIntegrationWebService
{
    /// <summary>
    /// Summary description for BuildService
    /// </summary>
    [WebService(Namespace = "http://buddybuildintegrationwebservice/buildservice",
        Description = "Acts as a proxy between users of the TFS Check-in Validation Tool and TFS. Allows decoupling " +
        "high-privileged permissions from the users and controlling them through the web service. " +
        "NOTE: To allow invoking the web methods from a remote host, make sure to uncomment the 'webServices' " +
        "section in the web.config file.")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    public class BuildService : System.Web.Services.WebService
    {
        #region Constants
        private const string BUDDYBUILD_MSBUILD_PROPERTY_TEMPLATE = "/p:{0} /p:BBWebService=true";
        private const string USER_AGENT_MAGIC_STRING = ".BuddyBuildOverride.";
        private const string TFS_CUSTOM_USER_AGENT = "Buddy Build Web Service";
        private const bool DEFAULT_DISABLE_MAGIC_STRING_VERIFICATION = false;
        private const string DISABLE_MAGIC_STRING_VERIFICATION_KEYNAME = "DisableMagicStringVerification";
        private const string IGNORE_OVERRIDEN_DROP_LOCATION_KEYNAME = "IgnoreOverridenDropLocation";
        private const string IGNORE_OVERRIDEN_PRIORITY_KEYNAME = "IgnoreOverridenPriority";
        private const bool DEFAULT_IGNORE_OVERRIDEN_DROP_LOCATION = false;
        private const bool DEFAULT_IGNORE_OVERRIDEN_PRIORITY = false;
        private const string ARGUMENTS_PARSE_EXPRESSION = @"(?:;*)(?<name>\w*)[=](""((?<value>.*?)(?<!\\)"")|(?<value>[\w]*))";
        #endregion

        #region Web Methods
        [WebMethod(Description = "Returns the current service version number. This is used by clients " +
            "in order to determine whether certain features are supported by the web service.")]
        public int GetServiceVersion()
        {
            return 1;
        }

        [WebMethod(Description = "Returns the current user identity for the request. " +
            "If the identity returned is the impersonated identity, this indicates a configuration issue. " +
            "Make sure the web service's Anonymous Authentication is disabled from the Directory Security tab in " +
            "the IIS properties for the web application.")]
        public string GetCurrentIdentity()
        {
            return this.Context.User.Identity.Name;
        }

        [WebMethod(Description = "Queues a build based on the provided paramters. " +
            "'tfsHostName' should have the following syntax: 'http://hostname:port' (for example: http://tfs01.codeplex.com:8080). " +
            "If specified, 'msbuildCommandLinePropertyValue' should contain the property value without the " +
            "leading '/p:'. 'buildAgentName', 'priority', and 'msbuildCommandLinePropertyValue' are optional. " +
            "If specified, priority can be one of the following (case-insensitive): 'Low', 'BelowNormal', 'Normal', " +
            "'AboveNormal', and 'High'. 'requestedFor' is also optional, and when set, should be set to a valid user " +
            "identity.")]
        public int QueueBuddyBuild(string tfsHostName,
                                   string teamProjectName,
                                   string buildDefinitionName,
                                   string buildAgentName,
                                   string priority,
                                   string msbuildCommandLinePropertyValue,
                                   string requestedFor)
        {
            return this.QueueBuild(tfsHostName,
                                          teamProjectName,
                                          buildDefinitionName,
                                          buildAgentName,
                                          priority,
                                          msbuildCommandLinePropertyValue,
                                          requestedFor,
                                          string.Empty,
                                          false);
        }

        [WebMethod(Description = "Queues a build based on the provided paramters. " +
            "'tfsHostName' should have the following syntax: 'http://hostname:port' (for example: http://tfs01.codeplex.com:8080). " +
            "If specified, 'msbuildCommandLinePropertyValue' should contain the property value without the " +
            "leading '/p:'. 'buildAgentName', 'priority', and 'msbuildCommandLinePropertyValue' are optional. " +
            "If specified, priority can be one of the following (case-insensitive): 'Low', 'BelowNormal', 'Normal', " +
            "'AboveNormal', and 'High'. 'requestedFor' is also optional, and when set, should be set to a valid user " +
            "identity. dropLocation is optional. When specified, it overrides the drop location specified in the build " +
            "definition.")]
        public int QueueBuddyBuildEx(string tfsHostName,
                                     string teamProjectName,
                                     string buildDefinitionName,
                                     string buildAgentName,
                                     string priority,
                                     string msbuildCommandLinePropertyValue,
                                     string requestedFor,
                                     string dropLocation)
        {
            return this.QueueBuild(tfsHostName,
                                          teamProjectName,
                                          buildDefinitionName,
                                          buildAgentName,
                                          priority,
                                          msbuildCommandLinePropertyValue,
                                          requestedFor,
                                          string.Empty,
                                          !this.IsBuddyBuildRequest(msbuildCommandLinePropertyValue));
        }

        [WebMethod(Description = "Queues a build based on the provided paramters. " +
            "'tfsHostName' should have the following syntax: 'http://hostname:port' (for example: http://tfs01.codeplex.com:8080). " +
            "If specified, 'msbuildCommandLinePropertyValue' should contain the property value without the " +
            "leading '/p:'. 'buildAgentName', 'priority', and 'msbuildCommandLinePropertyValue' are optional. " +
            "If specified, priority can be one of the following (case-insensitive): 'Low', 'BelowNormal', 'Normal', " +
            "'AboveNormal', and 'High'. 'requestedFor' is also optional, and when set, should be set to a valid user " +
            "identity. dropLocation is optional. When specified, it overrides the drop location specified in the build " +
            "definition. onlyRegularBuild is used to specify that the build to be queued is a regular build, so there is no " +
            "need to validate the built-in parameters.")]
        public int QueueBuild(string tfsHostName,
                                     string teamProjectName,
                                     string buildDefinitionName,
                                     string buildAgentName,
                                     string priority,
                                     string msbuildCommandLinePropertyValue,
                                     string requestedFor,
                                     string dropLocation,
                                     bool onlyRegularBuild)
        {
            try
            {
                string disableMagicStringVerificationSetting = ConfigurationSettings.AppSettings[DISABLE_MAGIC_STRING_VERIFICATION_KEYNAME];
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: disableMagicStringVerification: {0}.", disableMagicStringVerificationSetting ?? string.Empty));

                string ignoreOverridenDropLocationSetting = ConfigurationSettings.AppSettings[IGNORE_OVERRIDEN_DROP_LOCATION_KEYNAME];
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: disableMagicStringVerification: {0}.", ignoreOverridenDropLocationSetting ?? string.Empty));

                string ignoreOverridenPrioritySetting = ConfigurationSettings.AppSettings[IGNORE_OVERRIDEN_PRIORITY_KEYNAME];
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: disableMagicStringVerification: {0}.", ignoreOverridenPrioritySetting ?? string.Empty));

                bool disableVerification = string.IsNullOrEmpty(disableMagicStringVerificationSetting) ?
                    DEFAULT_DISABLE_MAGIC_STRING_VERIFICATION : bool.Parse(disableMagicStringVerificationSetting);

                bool ignoreOverridenDropLocation = string.IsNullOrEmpty(ignoreOverridenDropLocationSetting) ?
                    DEFAULT_IGNORE_OVERRIDEN_DROP_LOCATION : bool.Parse(ignoreOverridenDropLocationSetting);

                bool ignoreOverridenPriority = string.IsNullOrEmpty(ignoreOverridenPrioritySetting) ?
                    DEFAULT_IGNORE_OVERRIDEN_PRIORITY : bool.Parse(ignoreOverridenPrioritySetting);

                if (!disableVerification)
                {
                    // If the request is not coming from the buddy build service, reject it:
                    if (!this.Context.Request.UserAgent.Contains(USER_AGENT_MAGIC_STRING))
                        throw new Exception("Integration::QueueBuddyBuild: You are not authorized to call this service. If you are using the " +
                                            "VS2008 add-in and this error persists, please ask your administrator to set DisableMagicStringVerification " +
                                            "to 'true' in web.config.");
                }

                // validations.
                if (string.IsNullOrEmpty(tfsHostName))
                    throw new Exception("Integration::QueueBuddyBuild: Validation Error: tfsHostName null or empty.");
                if (string.IsNullOrEmpty(teamProjectName))
                    throw new Exception("Integration::QueueBuddyBuild: Validation Error: teamProjectName null or empty.");
                if (string.IsNullOrEmpty(buildDefinitionName))
                    throw new Exception("Integration::QueueBuddyBuild: Validation Error: buildDefinitionName null or empty.");

                tfsHostName = tfsHostName.Trim();
                teamProjectName = teamProjectName.Trim();
                buildDefinitionName = buildDefinitionName.Trim();
                buildAgentName = (buildAgentName == null) ? string.Empty : buildAgentName.Trim();
                priority = (priority == null) ? string.Empty : priority.Trim();
                msbuildCommandLinePropertyValue = (msbuildCommandLinePropertyValue == null) ? string.Empty : msbuildCommandLinePropertyValue.Trim();
                requestedFor = (requestedFor == null) ? string.Empty : requestedFor.Trim();

                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: tfsHostName: {0}.", tfsHostName));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: teamProjectName: {0}.", teamProjectName));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: buildDefinitionName: {0}.", buildDefinitionName));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: buildAgentName: {0}.", buildAgentName));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: priority: {0}.", priority));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: msbuildCommandLinePropertyValue: {0}.", msbuildCommandLinePropertyValue));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: requestedFor: {0}.", requestedFor));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: dropLocation: {0}.", dropLocation));

                // Re-parse the parameters to escape embedded semi-colons.
                msbuildCommandLinePropertyValue = this.PrepareCommandLineArguments(msbuildCommandLinePropertyValue);
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: msbuildCommandLinePropertyValue(re-parsed): {0}.", msbuildCommandLinePropertyValue));

                // Let's validate the custom parameters before embarking on any expensive operations:
                List<MsBuildProperty> validatedProperties = null;
                if (!this.ValidateConfiguredMsBuildPropertiesInternal(msbuildCommandLinePropertyValue, onlyRegularBuild, out validatedProperties))
                {
                    throw new Exception("Integration::QueueBuddyBuild: Validation failed for provided MSBuild paramters. " +
                    "Verify that the custom paramters you provided are configured on the web server.");
                }

                // TFS
                TeamFoundationServer.ApplicationName = TFS_CUSTOM_USER_AGENT;
                TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(tfsHostName);
                tfs.EnsureAuthenticated();

                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: Authenticated username: {0}.", tfs.AuthenticatedUserName));
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: Authenticated user display name: {0}.", tfs.AuthenticatedUserDisplayName));

                // Queue the build.
                IBuildServer buildServer = (IBuildServer)tfs.GetService(typeof(IBuildServer));

                Utils.LogMessage("Integration::QueueBuddyBuild: Getting build definition.");

                // Get the build definition for which a build is to be queued.
                IBuildDefinition definition = buildServer.GetBuildDefinition(teamProjectName, buildDefinitionName);
                if (definition == null)
                {
                    throw new Exception(string.Format(
                        "Integration::QueueBuddyBuild: Build definition not found: {0} in team project {1}, " +
                        "or invalid team project and build definition combination.",
                        buildDefinitionName, teamProjectName));
                }

                Utils.LogMessage("Integration::QueueBuddyBuild: Creating build request.");

                // Create a build request for the build definition.
                IBuildRequest request = definition.CreateBuildRequest();

                if (!string.IsNullOrEmpty(requestedFor))
                    request.RequestedFor = requestedFor;

                if (!string.IsNullOrEmpty(priority) && !ignoreOverridenPriority) // use default priority if none is provided.
                {
                    request.Priority = this.GetQueuePriorityFromString(priority);
                }
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: Effective Request Priority: {0}.", request.Priority.ToString()));

                //request.RequestedFor = username;
                if (!string.IsNullOrEmpty(msbuildCommandLinePropertyValue))
                {
                    request.CommandLineArguments = string.Format(
                        BUDDYBUILD_MSBUILD_PROPERTY_TEMPLATE,
                        this.SerializeMsBuildPropertiesList(validatedProperties));
                }

                // Override with a new drop location if one is provided:
                if (!string.IsNullOrEmpty(dropLocation) && !ignoreOverridenDropLocation)
                {
                    request.DropLocation = dropLocation;
                }

                if (!string.IsNullOrEmpty(buildAgentName)) // leave definition's agent if no override is provided.
                {
                    IBuildAgentSpec spec = buildServer.CreateBuildAgentSpec(teamProjectName, buildAgentName);
                    IBuildAgentQueryResult queryResult = buildServer.QueryBuildAgents(spec);
                    if (queryResult != null && queryResult.Agents.Length > 0)
                    {
                        request.BuildAgent = queryResult.Agents[0];
                    }
                    else
                    {
                        throw new Exception(string.Format(
                            "Integration::QueueBuddyBuild: Build agent not found: {0}. Leave this paramter blank to use default " +
                            "build agent associated with provide build definition.",
                            buildAgentName));
                    }
                }
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: Effective Build Agent: {0}.", request.BuildAgent.Name));
                IQueuedBuild queuedBuild = null;

                queuedBuild = buildServer.QueueBuild(request, QueueOptions.None);

                int queuedBuildId = queuedBuild.Id;
                Utils.LogMessage(string.Format("Integration::QueueBuddyBuild: Queued Successfully. Id: {0}.", queuedBuildId));

                return queuedBuildId;
            }
            catch (Exception ex)
            {
                Utils.LogMessage("Integration::QueueBuddyBuild: ErrorMessage: " + ex.Message);
                Utils.LogMessage("Integration::QueueBuddyBuild: StackTrace: " + ex.StackTrace);

                throw new Exception("Integration::QueueBuddyBuild: ErrorMessage: " + ex.Message);
                //throw new Exception("Integration::QueueBuddyBuild: ErrorMessage: " + ex.Message + ".\r\n" +
                //    "Stack: " + ex.StackTrace);
            }
        }

        [WebMethod(Description = "Returns a list of the built-in MSBuild properties used by the Buddy Build process.")]
        public List<MsBuildProperty> GetBuiltInProperties()
        {
            Utils.LogMessage("Integration::GetBuiltInProperties.");
            try
            {
                return this.GetBuiltInMsBuildProperties();
            }
            catch (Exception ex)
            {
                Utils.LogMessage("Integration::GetBuiltInProperties: ErrorMessage: " + ex.Message);
                Utils.LogMessage("Integration::GetBuiltInProperties: StackTrace: " + ex.StackTrace);

                throw new Exception("Integration::GetBuiltInProperties: ErrorMessage: " + ex.Message);
                //throw new Exception("Integration::GetBuiltInProperties: ErrorMessage: " + ex.Message + ".\r\n" +
                //    "Stack: " + ex.StackTrace);
            }
        }

        [WebMethod(Description = "Returns a list of the custom MSBuild properties that are configured for the Buddy " +
            "Build process.")]
        public List<MsBuildProperty> GetCustomProperties()
        {
            Utils.LogMessage("Integration::GetCustomProperties.");
            try
            {
                return this.GetCustomMsBuildProperties();
            }
            catch (Exception ex)
            {
                Utils.LogMessage("Integration::GetCustomProperties: ErrorMessage: " + ex.Message);
                Utils.LogMessage("Integration::GetCustomProperties: StackTrace: " + ex.StackTrace);

                throw new Exception("Integration::GetCustomProperties: ErrorMessage: " + ex.Message);
                //throw new Exception("Integration::GetCustomProperties: ErrorMessage: " + ex.Message + ".\r\n" +
                //    "Stack: " + ex.StackTrace);
            }
        }

        [WebMethod(Description = "Validates the semi-colon delimited list of MSBuild properties against the list of " +
            "custom properties configured on the web server.")]
        public bool ValidateConfiguredMsBuildProperties(string properties)
        {
            return ValidateConfiguredMsBuildPropertiesInternal(properties);
        }
        #endregion

        #region Private Methods
        private List<MsBuildProperty> GetBuiltInMsBuildProperties()
        {
            string filename = this.Server.MapPath("App_Data/BuiltInMSBuildProperties.xml");
            string xpath = "/ns:BuiltInProperties/ns:Property";
            string xmlNamespace = "http://schemas.microsoft.com/developer/buddybuildwebservice/2009/builtinproperties";

            return this.GetProperties(filename, xpath, xmlNamespace);
        }

        private List<MsBuildProperty> GetCustomMsBuildProperties()
        {
            string filename = this.Server.MapPath("App_Data/CustomMSBuildProperties.xml");
            string xpath = "/ns:TfsHosts/ns:Host/ns:TeamProject/ns:BuildDefinition/ns:Properties/ns:Property";
            string xmlNamespace = "http://schemas.microsoft.com/developer/buddybuildwebservice/2009/customproperties";

            return this.GetProperties(filename, xpath, xmlNamespace);
        }

        private List<MsBuildProperty> GetProperties(string filename, string xpath, string xmlNamespace)
        {
            Utils.LogMessage("Integration::GetProperties: filename: " + filename);
            Utils.LogMessage("Integration::GetProperties: xpath: " + xpath);
            Utils.LogMessage("Integration::GetProperties: xmlNamespace: " + xmlNamespace);

            if (!File.Exists(filename))
                return null;

            List<MsBuildProperty> properties = new List<MsBuildProperty>();

            XPathDocument document = new XPathDocument(filename);
            XPathNavigator navigator = document.CreateNavigator();
            XPathExpression builtInPropertiesQuery = navigator.Compile(xpath);
            XPathExpression nameQuery = navigator.Compile("ns:Name");
            XPathExpression defaultValueQuery = navigator.Compile("ns:DefaultValue");
            XPathExpression descriptionQuery = navigator.Compile("ns:Description");
            XPathExpression validationExpressionQuery = navigator.Compile("ns:ValidationExpression");
            XPathExpression dataTypeQuery = navigator.Compile("ns:PropertyDataType");
            XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
            manager.AddNamespace("ns", xmlNamespace);
            builtInPropertiesQuery.SetContext(manager);
            nameQuery.SetContext(manager);
            defaultValueQuery.SetContext(manager);
            descriptionQuery.SetContext(manager);
            validationExpressionQuery.SetContext(manager);
            dataTypeQuery.SetContext(manager);

            XPathNodeIterator ni = navigator.Select(builtInPropertiesQuery);
            if (null == ni || ni.Count <= 0)
            {
                Utils.LogMessage("GetProperties: Information: Found 0 properties.");

                return null;
            }

            Utils.LogMessage(
                string.Format("GetProperties: Information: " +
                "Found {0} properties.", ni.Count));

            int counter = 0;
            while (ni.MoveNext())
            {
                XPathNavigator node = ni.Current;
                XPathNavigator nameNode = node.SelectSingleNode(nameQuery);
                XPathNavigator defaultValueNode = node.SelectSingleNode(defaultValueQuery);
                XPathNavigator descriptionNode = node.SelectSingleNode(descriptionQuery);
                XPathNavigator validationExpressionNode = node.SelectSingleNode(validationExpressionQuery);
                XPathNavigator dataTypeNode = node.SelectSingleNode(dataTypeQuery);

                if (nameNode == null)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "The Name node was not found for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "The Name node was not found for property #{0} [0-based].", counter));
                }

                if (defaultValueNode == null)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "The DefaultValue node was not found for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "The DefaultValue node was not found for property #{0} [0-based].", counter));
                }

                if (descriptionNode == null)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "The Description node was not found for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "The Description node was not found for property #{0} [0-based].", counter));
                }

                if (validationExpressionNode == null)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Information: " +
                        "The Validation Expression node was not found for property #{0} [0-based].", counter));
                }

                if (dataTypeNode == null)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "The PropertyDataType node was not found for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "The PropertyDataType node was not found for property #{0} [0-based].", counter));
                }

                string name = nameNode.InnerXml;
                string defaultValue = defaultValueNode.InnerXml;
                string description = descriptionNode.InnerXml;
                string validationExpression = validationExpressionNode == null ? string.Empty : validationExpressionNode.InnerXml;
                string dataType = dataTypeNode.InnerXml;

                Utils.LogMessage(string.Format("GetProperties: Information: property #{0}: Name: \"{1}\".", counter, name));
                Utils.LogMessage(string.Format("GetProperties: Information: property #{0}: DefaultValue: \"{1}\".", counter, defaultValue));
                Utils.LogMessage(string.Format("GetProperties: Information: property #{0}: Description: \"{1}\".", counter, description));
                Utils.LogMessage(string.Format("GetProperties: Information: property #{0}: ValidationExpression: \"{1}\".", counter, validationExpression));
                Utils.LogMessage(string.Format("GetProperties: Information: property #{0}: PropertyDataType: \"{1}\".", counter, dataType));

                if (name.Trim() == string.Empty)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "The Name node is empty for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "The Name node is empty for property #{0} [0-based].", counter));
                }

                if (dataType.Trim() == string.Empty)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "The PropertyDataType node is empty for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "The PropertyDataType node is empty for property #{0} [0-based].", counter));
                }

                MsBuildProperty property = new MsBuildProperty()
                {
                    Name = name,
                    DefaultValue = defaultValue,
                    Description = description,
                    ValidationExpression = validationExpression,
                    PropertyDataType = dataType
                };

                if (Type.GetType(dataType, false) == null)
                {
                    Utils.LogMessage(
                        string.Format("GetProperties: Error: " +
                        "Invalid FrameworkDataType specified for property #{0} [0-based].", counter));

                    throw new Exception(
                        string.Format("GetProperties: Error: " +
                        "Invalid FrameworkDataType specified for property #{0} [0-based].", counter));
                }

                properties.Add(property);

                counter++;
            }

            return properties;
        }

        private bool ValidateConfiguredMsBuildPropertiesInternal(string properties)
        {
            List<MsBuildProperty> validatedProperties = null;
            return this.ValidateConfiguredMsBuildPropertiesInternal(properties, false, out validatedProperties);
        }

        private bool ValidateConfiguredMsBuildPropertiesInternal(string properties, bool onlyCustomProperties)
        {
            List<MsBuildProperty> validatedProperties = null;
            return this.ValidateConfiguredMsBuildPropertiesInternal(properties, onlyCustomProperties, out validatedProperties);
        }

        private bool ValidateConfiguredMsBuildPropertiesInternal(string properties, bool onlyCustomProperties, out List<MsBuildProperty> validatedProperties)
        {
            validatedProperties = null;
            try
            {
                // Get built-in properties:
                List<MsBuildProperty> builtInProperties = null;

                if (!onlyCustomProperties)
                {
                    builtInProperties = this.GetBuiltInMsBuildProperties();
                    if (builtInProperties == null || builtInProperties.Count == 0)
                    {
                        throw new Exception("No built-in properties found. " +
                            "Please review the built-in properties XML configuration file on the web server (BuiltInMSBuildProperties.xml).");
                    }
                }

                // Get the custom properties:
                List<MsBuildProperty> customProperties = this.GetCustomMsBuildProperties();

                if (customProperties == null || customProperties.Count == 0)
                {
                    customProperties = new List<MsBuildProperty>() { };
                }

                if (!onlyCustomProperties)
                    customProperties.AddRange(builtInProperties);

                validatedProperties = customProperties; // Intermediate result. Initialize with default configured values.

                if (string.IsNullOrEmpty(properties))
                    return true;

                // Then, prepare the properties sent from the client for validation.
                string[] propertyNameValues = properties.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                if (propertyNameValues.Length == 0)
                    return true;

                Hashtable clientProperties = new Hashtable(propertyNameValues.Length);

                for (int i = 0; i < propertyNameValues.Length; i++)
                {
                    Utils.LogMessage(string.Format("Integration::ValidateConfiguredMsBuildProperties: PropertyNameValues[{0}]: \"{1}\".",
                        i, propertyNameValues[i]));
                    string[] property = propertyNameValues[i].Split(new char[] { '=' }, StringSplitOptions.None);
                    if (property.Length != 2)
                        throw new Exception("Invalid properties submitted. " +
                            "Please make sure the custom properties sent from the client match the server definitions.");

                    clientProperties.Add(property[0], property[1]);
                }

                if (clientProperties.Count == 0)
                {
                    return true;
                }

                // TODO: Refactor: Extract the code that follows to a separate method.
                // Validate.
                foreach (DictionaryEntry item in clientProperties)
                {
                    string propertyName = (string)item.Key;
                    string propertyValue = this.UnEscapePropertyValue((string)item.Value);

                    MsBuildProperty property = customProperties.Find((MsBuildProperty searchProperty) =>
                    {
                        return (searchProperty.Name.Equals(propertyName, StringComparison.InvariantCulture));
                    });
                    if (property != null)
                    {
                        // If an empty value was passed, that's an indication to use the default value configured.
                        if (string.IsNullOrEmpty(propertyValue))
                            propertyValue = property.DefaultValue;

                        // no need to check for null as the GetProperties() method has taken care of that.
                        Type propertyDataType = Type.GetType(property.PropertyDataType, false);

                        Utils.LogMessage(string.Format("Integration::ValidateConfiguredMsBuildProperties: Property Name=\"{0}\" " +
                            "- Property Value=\"{1}\" - Configured Data Type=\"{2}\".",
                            propertyName, propertyValue, property.PropertyDataType));

                        object propertyInstance = null;
                        try
                        {
                            propertyInstance = Convert.ChangeType(propertyValue, propertyDataType);
                        }
                        catch (Exception ex)
                        {
                            throw new Exception(string.Format("Data type validation failed: {0}: Property Name=\"{1}\" " +
                                "- Property Value=\"{2}\" - Configured Data Type=\"{3}\".",
                                ex.Message, propertyName, propertyValue, property.PropertyDataType), ex);
                        }

                        if (propertyInstance == null)
                        {
                            throw new Exception(string.Format("Data type validation failed: Property value is of invalid datatype: Property Name=\"{0}\" " +
                                "- Property Value=\"{1}\" - Configured Data Type=\"{2}\".",
                                propertyName, propertyValue, property.PropertyDataType));
                        }
                        else
                        {
                            Utils.LogMessage(string.Format("Integration::ValidateConfiguredMsBuildProperties (Post Validation): Property Name=\"{0}\" " +
                                "- Property Value=\"{1}\" - Converted Value=\"{2}\".",
                                propertyName, propertyValue, propertyInstance));

                            if (string.IsNullOrEmpty(propertyValue))
                            {
                                continue; // leave the default configured value.
                            }

                            // Let's do extra vaidation if the regular expression is provided.
                            string validationExpression = property.ValidationExpression;

                            if (!string.IsNullOrEmpty(validationExpression))
                            {
                                if (!Regex.IsMatch((string)propertyValue, validationExpression))
                                {
                                    throw new Exception(string.Format("Regular expression validation failed: Property Name=\"{0}\" " +
                                        "- Property Value=\"{1}\" - Configured Data Type=\"{2}\" - Validation Expression=\"{3}\".",
                                        propertyName, propertyValue, property.PropertyDataType, validationExpression));
                                }
                            }
                            property.DefaultValue = propertyValue;
                        }
                    }
                    else
                    {
                        throw new Exception(string.Format("Property not configured on server: Property Name=\"{0}\" " +
                            "- Property Value=\"{1}\".",
                            propertyName, propertyValue));
                    }
                }
                validatedProperties = customProperties; // the latest version that is updated with client values.

                return true;
            }
            catch (Exception ex)
            {
                Utils.LogMessage("Integration::ValidateConfiguredMsBuildProperties: ErrorMessage: " + ex.Message);
                Utils.LogMessage("Integration::ValidateConfiguredMsBuildProperties: StackTrace: " + ex.StackTrace);

                if (ex.InnerException != null)
                {
                    Utils.LogMessage("Integration::ValidateConfiguredMsBuildProperties: InnerException.ErrorMessage: " + ex.InnerException.Message);
                    Utils.LogMessage("Integration::ValidateConfiguredMsBuildProperties: InnerException.StackTrace: " + ex.InnerException.StackTrace);
                }

                throw new Exception("Integration::ValidateConfiguredMsBuildProperties: ErrorMessage: " + ex.Message);
                //throw new Exception("Integration::ValidateConfiguredMsBuildProperties: ErrorMessage: " + ex.Message + ".\r\n" +
                //    "Stack: " + ex.StackTrace);
            }
        }

        private string UnEscapePropertyValue(string propertyValue)
        {
            if (string.IsNullOrEmpty(propertyValue))
                return propertyValue;

            if (!(propertyValue[0] == '\"' && propertyValue[propertyValue.Length - 1] == '\"'))
                return propertyValue;

            return this.UnEscapePropertyValue(propertyValue.Substring(1, propertyValue.Length - 2));
        }

        private string SerializeMsBuildPropertiesList(List<MsBuildProperty> propertiesList)
        {
            if (propertiesList != null && propertiesList.Count > 0)
            {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < propertiesList.Count; i++)
                {
                    sb.AppendFormat("{0}=\"{1}\"",
                        propertiesList[i].Name,
                        this.MsBuildEncodeForLists(this.UnEscapePropertyValue(propertiesList[i].DefaultValue)));

                    if (i < propertiesList.Count - 1)
                        sb.Append(";");
                }
                return sb.ToString();
            }
            return string.Empty;
        }

        private string MsBuildEncodeForLists(string rawMsBuildPropertyValue)
        {
            // Escape ; and =
            return rawMsBuildPropertyValue.Replace(";", "%3B").Replace("=", "%3D");
        }

        private bool IsBuddyBuildRequest(string msBuildProperties)
        {
            if (string.IsNullOrEmpty(msBuildProperties))
                return false;

            string[] properties = msBuildProperties.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
            if (properties != null && properties.Length > 0)
            {
                for (int i = 0; i < properties.Length; i++)
                {
                    if (properties[i].ToLower().Equals("IsBuddyBuildV2=True", StringComparison.InvariantCultureIgnoreCase))
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        private string PrepareCommandLineArguments(string inputArguuments)
        {
            try
            {
                Regex regex = new Regex(ARGUMENTS_PARSE_EXPRESSION,
                                RegexOptions.IgnoreCase |
                                RegexOptions.Compiled |
                                RegexOptions.Singleline);
                MatchCollection matches = regex.Matches(inputArguuments);
                if (matches == null || matches.Count == 0)
                    return inputArguuments;

                Utils.LogMessage("Integration::PrepareCommandLineArguments: matches found: " + matches.Count);

                StringBuilder parsedString = new StringBuilder();
                for (int i = 0; i < matches.Count; i++)
                {
                    string argumentName = matches[i].Groups["name"].Value;
                    string argumentValue = matches[i].Groups["value"].Value;
                    argumentValue = this.MsBuildEncodeForLists(argumentValue);
                    Utils.LogMessage("Integration::PrepareCommandLineArguments: name: " + argumentName);
                    Utils.LogMessage("Integration::PrepareCommandLineArguments: value: " + argumentValue);
                    parsedString.AppendFormat("{0}=\"{1}\"", argumentName, argumentValue);
                    if (i != matches.Count - 1)
                        parsedString.Append(";");
                }
                return parsedString.ToString();
            }
            catch (Exception ex)
            {
                Utils.LogMessage("Integration::PrepareCommandLineArguments: ErrorMessage: " + ex.Message);
                Utils.LogMessage("Integration::PrepareCommandLineArguments: StackTrace: " + ex.StackTrace);

                return inputArguuments;
            }
        }

        private QueuePriority GetQueuePriorityFromString(string queuePriority)
        {
            if (string.IsNullOrEmpty(queuePriority))
                return QueuePriority.Normal;

            switch (queuePriority.ToLower())
            {
                case "high":
                    return QueuePriority.High;
                case "abovenormal":
                    return QueuePriority.AboveNormal;
                case "belownormal":
                    return QueuePriority.BelowNormal;
                case "low":
                    return QueuePriority.Low;
                default:
                    return QueuePriority.Normal;
            }
        }
        #endregion
    }
}
